﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.IO;

namespace Weka
{
    internal abstract class ObjectDeserialiserProvider
    {
        public abstract object Deserialise(StreamReada sr);
        public static ObjectDeserialiserProvider Create<T>(ObjectDeserialiserProviderCreator dzx)
        where T : new()
        {
            return new ObjectDeserialiserProvider<T>(dzx);
        }
    }

    internal sealed class ObjectDeserialiserProvider<T> : ObjectDeserialiserProvider
        where T : new()
    {
        private readonly FieldDeserialiserProvider<T>[] _fieldDeserialiserProviders;
        private readonly Action<T> _missingValueSetter;
        public ObjectDeserialiserProvider(ObjectDeserialiserProviderCreator dzx)
        {
            var fop = typeof(T).GetAllProperties()
                        .Where(p => p.CanWrite)
                        .Select(p => new { M = p.GetSetMethod(true), FA = (FieldAttribute)p.GetCustomAttributes(typeof(FieldAttribute), false).FirstOrDefault() })
                        .Where(f => f.FA != null);

            var fom = typeof(T).GetAllMethods()
                        .Where(m => m.ReturnType == typeof(void) && m.GetParameters().Length == 1)
                        .Select(m => new { M = m, FA = (FieldAttribute)m.GetCustomAttributes(typeof(FieldAttribute), false).FirstOrDefault() })
                        .Where(f => f.FA != null);

            var fooo = fop.Union(fom).GroupBy(q => q.FA.Id, q => q).ToDictionary(w => w.Key, w => w.First());

            using (var ms = new MemoryStream(dzx.Key.GetRawData()))
            {
                var sr = new StreamReada(ms);

                int fieldCount = sr.ReadByte();

                var fieldIds = new byte[fieldCount];
                var fdps = new FieldDeserialiserProvider<T>[fieldCount];
                int messageIndex = 0;
                for (int i = 0; i < fieldCount; i++)
                {
                    var fieldInfo = sr.ReadFieldInfoAndSema();
                    fieldIds[i] = fieldInfo.FieldId;

                    var ax = fooo.TryGet(fieldInfo.FieldId);
                    MethodInfo mi = (ax == null) ? null : ax.M;
                    if (fieldInfo.FieldType == FieldType.PrimaryData)
                    {
                        fdps[i] = CreateFieldDeserialiserProvider(fieldInfo, mi, dzx);
                    }
                    else
                    {
                        var dzxSubMessage = dzx.CreateNewForSubMessage(messageIndex);
                        if (fieldInfo.FieldType == FieldType.Message)
                            fdps[i] = CreateFieldMessageDeserialiserProvider(fieldInfo, mi, dzxSubMessage);
                        else
                            fdps[i] = CreateFieldMessageListDeserialiserProvider(fieldInfo, mi, dzxSubMessage);

                        messageIndex++;
                    }
                }
                _fieldDeserialiserProviders = fdps;

                _missingValueSetter = CreateMissingValueSetter(fooo.Keys.Except(fieldIds).Select(fid => fooo[fid].FA.Id).ToArray());

            }
        }

        private Action<T> CreateMissingValueSetter(byte[] missingFieldIds)
        {
            if (missingFieldIds.Length == 0) return null;

            var fom = typeof(T)
                        .GetAllMethods()
                        .FirstOrDefault(m =>
                                    m.ReturnType == typeof(void) &&
                                    m.GetParameters().Length == 1 &&
                                    m.GetParameters()[0].ParameterType == typeof(IEnumerable<byte>) &&
                                    m.GetCustomAttributes(typeof(MissingValueSetterAttribute), false).Any());

            if (fom == null) return null;

            Action<T, IEnumerable<byte>> set = (Action<T, IEnumerable<byte>>)Delegate.CreateDelegate(typeof(Action<T, IEnumerable<byte>>), null, fom);


            return t => set(t, missingFieldIds);
        }




        private FieldDeserialiserProvider<T> CreateFieldMessageDeserialiserProvider(FieldInfo fieldInfo, MethodInfo methodInfo, ObjectDeserialiserProviderCreator dzx)
        {
            //Todo check for null methodinfo
            Type messageType = dzx._deserliser.GetMessageType(fieldInfo.MessageId);
            //TODO check to message not found i.e. null.
            var createGeneric = typeof(FieldDeserialiserProviderMessage).GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
            var createSpecfic = createGeneric.MakeGenericMethod(typeof(T), messageType);
            FieldDeserialiserProvider<T> e2 = (FieldDeserialiserProvider<T>)createSpecfic.Invoke(null, new object[] { dzx, methodInfo });
            return e2;
        }

        private FieldDeserialiserProvider<T> CreateFieldMessageListDeserialiserProvider(FieldInfo fieldInfo, MethodInfo methodInfo, ObjectDeserialiserProviderCreator dzx)
        {
            //  throw new NotImplementedException();
            //Todo check for null methodinfo
            Type messageType = dzx._deserliser.GetMessageType(fieldInfo.MessageId);
            //TODO check to message not found i.e. null.
            var createGeneric = typeof(FieldDeserialiserProviderMessageList).GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
            var createSpecfic = createGeneric.MakeGenericMethod(typeof(T), messageType);
            FieldDeserialiserProvider<T> e2 = (FieldDeserialiserProvider<T>)createSpecfic.Invoke(null, new object[] { dzx, methodInfo });
            return e2;
        }


        private static Action<TV, TR> CreateActionDelegate<TV, TR>(MethodInfo method)
        {
            if (method == null)
                return (a, b) => { };//No matching fieldId
            else
                return method.CreateActionDelegate<TV, TR>();
        }
        private FieldDeserialiserProvider<T> CreateFieldDeserialiserProvider(FieldInfo fieldInfo, MethodInfo methodInfo, ObjectDeserialiserProviderCreator dzx)
        {
            if (fieldInfo.DataType == DataType.Int32)
                return CreateFieldDeserialiserProviderInt32(methodInfo);

            if (fieldInfo.DataType == DataType.Boolean)
                return new FieldDeserialiserProviderBoolean<T>() { Writer = CreateActionDelegate<T, bool>(methodInfo) };

            if (fieldInfo.DataType == DataType.BooleanNullable)
                return new FieldDeserialiserProviderBooleanN<T>() { Writer = CreateActionDelegate<T, bool?>(methodInfo) };

            if (fieldInfo.DataType == DataType.Int16)
                return CreateFieldDeserialiserProviderInt16(methodInfo);

            if (fieldInfo.DataType == DataType.String)
                return new FieldDeserialiserProviderString<T>() { Writer = CreateActionDelegate<T, string>(methodInfo) };

            if (fieldInfo.DataType == DataType.DateTime)
                return CreateFieldDeserialiserProviderStruct<DateTime>(methodInfo,
                                                            w => new FieldDeserialiserProviderDateTime<T> { Writer = w },
                                                            w => new FieldDeserialiserProviderDateTimeN<T> { Writer = w });

            if (fieldInfo.DataType == DataType.Decimal)
                return CreateFieldDeserialiserProviderStruct<decimal>(methodInfo,
                                    w => new FieldDeserialiserProviderGen<T, FieldDeserialiserDecimal<T>, decimal> { Writer = w },
                                    w => new FieldDeserialiserProviderGenN<T, FieldDeserialiserDecimalN<T>, decimal> { Writer = w });

            return null;//TODO
        }

        private static FieldDeserialiserProvider<T> CreateFieldDeserialiserProviderStruct<TStruct>(MethodInfo methodInfo, Func<Action<T, TStruct>, FieldDeserialiserProvider<T>> fun, Func<Action<T, TStruct?>, FieldDeserialiserProvider<T>> funNull)
        where TStruct : struct
        {
            if (methodInfo == null)
                return funNull(CreateActionDelegate<T, TStruct?>(methodInfo));

            Type t = methodInfo.GetSetType();

            if (t == typeof(TStruct))
                return fun(CreateActionDelegate<T, TStruct>(methodInfo));

            if (t == typeof(TStruct?))
                return funNull(CreateActionDelegate<T, TStruct?>(methodInfo));

            return funNull((a, b) => { throw new Exception("Typemismatch"); });//TODO
        }

        private static FieldDeserialiserProvider<T> CreateFieldDeserialiserProviderInt32(MethodInfo methodInfo)
        {
            return CreateFieldDeserialiserProviderInt<Int32>(
                                    methodInfo,
                                    w => new FieldDeserialiserProviderInt32<T> { Writer = w },
                                    w => new FieldDeserialiserProviderInt32N<T> { Writer = w });
        }

        private static FieldDeserialiserProvider<T> CreateFieldDeserialiserProviderInt16(MethodInfo methodInfo)
        {
            return CreateFieldDeserialiserProviderInt<Int16>(
                                    methodInfo,
                                    w => new FieldDeserialiserProviderInt16<T> { Writer = w },
                                    w => new FieldDeserialiserProviderInt16N<T> { Writer = w });
        }



        private static FieldDeserialiserProvider<T> CreateFieldDeserialiserProviderInt<TInt>(MethodInfo methodInfo, Func<Action<T, TInt>, FieldDeserialiserProvider<T>> fun1, Func<Action<T, TInt?>, FieldDeserialiserProvider<T>> fun2)
            where TInt : struct
        {
            if (methodInfo == null)
                return fun2(CreateActionDelegate<T, TInt?>(methodInfo));

            Type t = methodInfo.GetSetType();

            if (t == typeof(TInt))
                return fun1(CreateActionDelegate<T, TInt>(methodInfo));

            if (t == typeof(TInt?))
                return fun2(CreateActionDelegate<T, TInt?>(methodInfo));

            if (t.IsEnum)
                return fun1(methodInfo.CreateEnumSetterDelegate<T, TInt>());

            if (t.IsNullable() && t.GetNullableType().IsEnum)
                return fun2(methodInfo.CreateEnumNullableSetterDelegate<T, TInt>());

            Action<T, TInt?> errorMessage = CreateErrorAction<TInt>(methodInfo);
            return fun2(errorMessage);
        }

        private static Action<T, TInt?> CreateErrorAction<TInt>(MethodInfo methodInfo) where TInt : struct
        {
            Type te = methodInfo.GetParameters()[0].ParameterType;//TODO

            string em = string.Format("TypeMismatch. Could not convert {0} to {1} for {2}.{3}.",
                            typeof(TInt).FullName, te.FullName, methodInfo.DeclaringType.FullName, methodInfo.Name);

            return (a, b) => { throw new Exception(em); };
        }



        internal ObjectDeserialiser<T> Create()
        {
            FieldDeserialiser<T>[] fds = new FieldDeserialiser<T>[_fieldDeserialiserProviders.Length];
            for (int i = 0; i < fds.Length; i++) fds[i] = _fieldDeserialiserProviders[i].CreateFieldDeserialiser();
            return new ObjectDeserialiser<T>
            {
                FieldDeserialisers = fds,
                MissingValueSetter = _missingValueSetter
            };
        }

        public override object Deserialise(StreamReada sr)
        {
            return Create().FromData(sr);
        }
    }
}